热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

跨度|可能会_GPUNvidiaCUDA编程基础教程——使用CUDAC/C++加速应用程序

篇首语:本文由编程笔记#小编为大家整理,主要介绍了GPUNvidiaCUDA编程基础教程——使用CUDAC/C++加速应用程序相关的知识,希望对你有一定的参考价值。

篇首语:本文由编程笔记#小编为大家整理,主要介绍了GPUNvidia CUDA 编程基础教程——使用 CUDA C/C++ 加速应用程序相关的知识,希望对你有一定的参考价值。


博主未授权任何人或组织机构转载博主任何原创文章,感谢各位对原创的支持!
博主链接



本人就职于国际知名终端厂商,负责modem芯片研发。
在5G早期负责终端数据业务层、核心网相关的开发工作,目前牵头6G算力网络技术标准研究。


博客内容主要围绕:
       5G/6G协议讲解
       算力网络讲解(云计算,边缘计算,端计算)
       高级C语言讲解
       Rust语言讲解





Nvidia CUDA编程基础教程


加速计算正在取代 CPU 计算,成为最佳计算做法。加速计算带来的层出不穷的突破性进展、对加速应用程序日益增长的需求、轻松编写加速计算的编程规范以及支持加速计算的硬件的不断改进,所有这一切都在推动计算方式必然会过渡到加速计算。

无论是从出色的性能还是易用性来看,CUDA 计算平台均是加速计算的制胜法宝。CUDA 提供一种可扩展 C、C++、Python 和 Fortran 等语言的编码范式,能够在世界上性能超强劲的并行处理器 NVIDIA GPU 上运行大量经加速的并行代码。CUDA 可以毫不费力地大幅加速应用程序,具有适用于 DNN、BLAS、图形分析 和 FFT 等的高度优化库生态系统,并且还附带功能强大的 命令行 和 可视化分析器。

CUDA 支持以下领域的许多(即便不是大多数)世界上性能超强劲的应用程序:计算流体动力学、分子动力学、量子化学、物理学 和高性能计算 (HPC)。

学习 CUDA 将能助您加速自己的应用程序。加速应用程序的执行速度远远超过 CPU 应用程序,并且可以执行 CPU 应用程序受限于其性能而无法执行的计算。在本实验中, 您将学习使用 CUDA C/C++ 为加速应用程序编程的入门知识,这些入门知识足以让您开始加速自己的 CPU 应用程序以获得性能提升并助您迈入全新的计算领域。



如何安装编译和运行环境,可以参考官方指导



GPU 加速应用程序与 CPU 应用程序对比

在 CPU 应用程序中,数据在 CPU 上进行分配,并且所有工作均在 CPU 上执行👇

而在加速应用程序中,则可使用 cudaMallocManaged()分配数据,其数据可由 CPU 进行访问和处理,并能自动迁移至可执行并行工作的 GPU👇

GPU 异步执行工作,与此同时CPU可执行它的工作,通过 cudaDeviceSynchronize(),CPU 代码可与异步 GPU 工作实现同步,并等待后者完成👇

经 CPU 访问的数据将会自动迁移👇




为GPU编写应用程序代码

CUDA 为许多常用编程语言提供扩展,例如 C、C++、Python 和 Fortran 等语言。CUDA 加速程序的文件扩展名是.cu

以下是一个 .cu 文件。其中包含两个函数,第一个函数将在 CPU 上运行,第二个将在 GPU 上运行。

void CPUFunction()

printf("This function is defined to run on the CPU.\\n");

__global__ void GPUFunction()

printf("This function is defined to run on the GPU.\\n");

int main()

CPUFunction();
GPUFunction<<<1, 1>>>();
cudaDeviceSynchronize();

以下是一些需要特别注意的重要代码行&#xff0c;以及加速计算中使用的一些其他常用术语&#xff1a;

__global__ void GPUFunction()


  • __global__ 关键字表明以下函数将在 GPU 上运行并可全局调用&#xff1b;
  • 常&#xff0c;我们将在 CPU 上执行的代码称为主机代码&#xff0c;而将在 GPU 上运行的代码称为设备代码&#xff1b;
  • 注意返回类型为 void。使用 __global__ 关键字定义的函数需要返回 void 类型。

GPUFunction<<<1, 1>>>();


  • 通常&#xff0c;当调用要在 GPU 上运行的函数时&#xff0c;我们将此种函数称为已启动的核函数&#xff1b;
  • 启动核函数时&#xff0c;我们必须提供执行配置&#xff0c;即在向核函数传递任何预期参数之前使用 <<<… >>> 语法完成的配置;
  • 在宏观层面&#xff0c;程序员可通过执行配置为核函数启动指定线程层次结构&#xff0c;从而定义线程组&#xff08;称为线程块&#xff09;的数量&#xff0c;以及要在每个线程块中执行的线程数量。

cudaDeviceSynchronize();


  • 与许多 C/C&#43;&#43; 代码不同&#xff0c;核函数启动方式为异步&#xff1a;CPU 代码将继续执行而无需等待核函数完成启动&#xff1b;
  • 调用 CUDA 运行时提供的函数 cudaDeviceSynchronize 将导致主机 (CPU) 代码暂作等待&#xff0c;直至设备 (GPU) 代码执行完成&#xff0c;才能在 CPU 上恢复执行。

编译并运行加速后的CUDA代码

CUDA 平台附带 NVIDIA CUDA 编译器 nvcc&#xff0c;可以编译 CUDA 加速应用程序&#xff0c;其中包含主机和设备代码。就本实验而言&#xff0c;nvcc 的讨论范围将根据我们的迫切需求据实确定。完成本实验学习后&#xff0c;有意深究 nvcc 的所有用户均可从 文档 开始入手。

曾使用过 gcc 的用户会对 nvcc 感到非常熟悉。例如&#xff0c;编译 some-CUDA.cu 文件就很简单&#xff1a;
nvcc -arch&#61;sm_70 -o out some-CUDA.cu -run 含义如下&#xff1a;


  • nvcc 是使用 nvcc 编译器的命令行命令&#xff1b;
  • some-CUDA.cu 作为文件传递以进行编译&#xff1b;
  • o 标志用于指定编译程序的输出文件;
  • arch 标志表示该文件必须编译为哪个架构类型;
  • run 标志将执行已成功编译的二进制文件。



CUDA的线程层次结构

GPU 可并行执行工作&#xff0c;GPU 在线程中执行工作&#xff0c;多个线程并行运行&#xff0c;如下图&#x1f447;

线程的集合称为块&#xff0c;块的数量很多&#xff0c;如下图&#x1f447;

与给定核函数启动相关联的块的集合称为网格&#xff0c;如下图&#x1f447;

GPU 函数称为核函数&#xff0c;核函数通过执行配置启动&#xff0c;执行配置定义了网格中的块数以及每个块中的线程数&#xff0c;网格中的每个块均包含相同数量的线程。


启动并行运行的核函数

程序员可通过执行配置指定有关如何启动核函数以在多个 GPU 线程中并行运行的详细信息。更准确地说&#xff0c;程序员可通过执行配置指定线程组&#xff08;称为线程块或简称为块&#xff09;数量以及其希望每个线程块所包含的线程数量。执行配置的语法如下&#xff1a;
<<>>

启动核函数时&#xff0c;核函数代码由每个已配置的线程块中的每个线程执行

因此&#xff0c;如果假设已定义一个名为 someKernel 的核函数&#xff0c;则下列情况为真&#xff1a;


  • someKernel<<<1, 1>>() 配置为在具有单线程的单个线程块中运行后&#xff0c;将只运行一次。
  • someKernel<<<1, 10>>() 配置为在具有 10 线程的单个线程块中运行后&#xff0c;将运行 10 次。
  • someKernel<<<10, 1>>() 配置为在 10 个线程块&#xff08;每个均具有单线程&#xff09;中运行后&#xff0c;将运行 10 次。
  • someKernel<<<10, 10>>() 配置为在 10 个线程块&#xff08;每个均具有 10 线程&#xff09;中运行后&#xff0c;将运行 100 次。



CUDA提供的线程层次结构变量

在核函数定义中&#xff0c;CUDA 提供的变量描述了它所执行的线程、块和网格。

gridDim.x 是网格中的块数&#xff0c;在本例中为 2 &#x1f447;

blockIdx.x 是网格中当前块的索引&#xff0c;在本例中为 0 &#x1f447;

blockIdx.x 是网格中当前块的索引&#xff0c;在本例中为 1 &#x1f447;

在核函数中&#xff0c;blockDim.x 描述了块中的线程数。在本例中为 4 &#x1f447;

网格中的所有块均包含相同数量的线程&#xff0c;在核函数中&#xff0c;threadIdx.x 描述了块中所包含线程的索引。在本例中为 0 &#x1f447;


线程和块的索引

每个线程在其线程块内部均会被分配一个索引&#xff0c;从 0 开始。此外&#xff0c;每个线程块也会被分配一个索引&#xff0c;并从 0 开始。正如线程组成线程块&#xff0c;线程块又会组成网格&#xff0c;而网格是 CUDA 线程层次结构中级别最高的实体。简言之&#xff0c;CUDA 核函数在由一个或多个线程块组成的网格中执行&#xff0c;且每个线程块中均包含相同数量的一个或多个线程。

CUDA 核函数可以访问能够识别如下两种索引的特殊变量&#xff1a;正在执行核函数的线程&#xff08;位于线程块内&#xff09;索引和线程所在的线程块&#xff08;位于网格内&#xff09;索引。这两种变量分别为 threadIdx.x 和 blockIdx.x。




协调并行线程

假设数据位于索引为 0 的向量中&#xff0c;由于某种未知原因&#xff0c;必须映射每个线程以处理向量中的元素&#xff0c;

通过这些变量&#xff0c;公式 threadIdx.x &#43; blockIdx.x * blockDim.x 可将每个线程映射到向量的元素中&#xff0c;


调整线程块的大小以实现更多的并行化

线程块包含的线程具有数量限制&#xff1a;确切地说是 1024 个。为增加加速应用程序中的并行量&#xff0c;我们必须要能在多个线程块之间进行协调。

CUDA 核函数可以访问给出块中线程数的特殊变量&#xff1a;blockDim.x。通过将此变量与 blockIdx.x 和 threadIdx.x 变量结合使用&#xff0c;并借助惯用表达式 threadIdx.x &#43; blockIdx.x * blockDim.x 在包含多个线程的多个线程块之间组织并行执行&#xff0c;并行性将得以提升。以下是详细示例。

执行配置 <<<10, 10>>> 将启动共计拥有 100 个线程的网格&#xff0c;这些线程均包含在由 10 个线程组成的 10 个线程块中。因此&#xff0c;我们希望每个线程&#xff08;0 至 99 之间&#xff09;都能计算该线程的某个唯一索引。


  • 如果线程块 blockIdx.x 等于 0&#xff0c;则 blockIdx.x * blockDim.x 为 0。向 0 添加可能的 threadIdx.x 值&#xff08;0 至 9&#xff09;&#xff0c;之后便可在包含 100 个线程的网格内生成索引 0 至 9&#xff1b;
  • 如果线程块 blockIdx.x 等于 1&#xff0c;则 blockIdx.x * blockDim.x 为 10。向 10 添加可能的 threadIdx.x 值&#xff08;0 至 9&#xff09;&#xff0c;之后便可在包含 100 个线程的网格内生成索引 10 至 19&#xff1b;
  • 如果线程块 blockIdx.x 等于 5&#xff0c;则 blockIdx.x * blockDim.x 为 50。向 50 添加可能的 threadIdx.x 值&#xff08;0 至 9&#xff09;&#xff0c;之后便可在包含 100 个线程的网格内生成索引 50 至 59&#xff1b;
  • 如果线程块 blockIdx.x 等于 9&#xff0c;则 blockIdx.x * blockDim.x 为 90。向 90 添加可能的 threadIdx.x 值&#xff08;0 至 9&#xff09;&#xff0c;之后便可在包含 100 个线程的网格内生成索引 90 至 99。



分配将要在GPU和CPU上访问的内存

CUDA 的最新版本&#xff08;版本 6 和更高版本&#xff09;已能轻松分配可用于 CPU 主机和任意数量 GPU 设备的内存。尽管现今有许多适用于内存管理并可支持加速应用程序中最优性能的中高级技术&#xff0c;但我们现在要介绍的基础 CUDA 内存管理技术不但能够支持远超 CPU 应用程序的卓越性能&#xff0c;而且几乎不会产生任何开发人员成本。

如要分配和释放内存&#xff0c;并获取可在主机和设备代码中引用的指针&#xff0c;请使用 cudaMallocManagedcudaFree 取代对 mallocfree 的调用&#xff0c;如下例所示&#xff1a;

// CPU-only
int N &#61; 2<<20;
size_t size &#61; N * sizeof(int);
int *a;
a &#61; (int *)malloc(size);
// Use &#96;a&#96; in CPU-only program.
free(a);
// Accelerated
int N &#61; 2<<20;
size_t size &#61; N * sizeof(int);
int *a;
// Note the address of &#96;a&#96; is passed as first argument.
cudaMallocManaged(&a, size);
// Use &#96;a&#96; on the CPU and/or on any GPU in the accelerated system.
cudaFree(a);



网格大小与工作量不匹配

在先前场景中&#xff0c;网络中的线程数与元素数量完全匹配&#xff0c;

如果线程数超过要完成的工作量&#xff0c;该怎么办&#xff1f;尝试访问不存在的元素会导致运行时错误&#xff0c;

必须使用代码检查并确保经由公式 threadIdx.x &#43; blockIdx.x * blockDim.x 计算出的 dataIndex 小于 N&#xff08;数据元素数量&#xff09;。


如何处理块配置与所需线程数不匹配

可能会出现这样的情况&#xff0c;执行配置所创建的线程数无法匹配为实现并行循环所需的线程数。

一个常见的例子与希望选择的最佳线程块大小有关。例如&#xff0c;鉴于 GPU 的硬件特性&#xff0c;所含线程的数量为 32 的倍数的线程块是最理想的选择&#xff0c;因其具备性能上的优势。假设我们要启动一些线程块且每个线程块中均包含 256 个线程&#xff08;32 的倍数&#xff09;&#xff0c;并需运行 1000 个并行任务&#xff08;此处使用极小的数量以便于说明&#xff09;&#xff0c;则任何数量的线程块均无法在网格中精确生成 1000 个总线程&#xff0c;因为没有任何整数值在乘以 32 后可以恰好等于 1000。

这个问题可以通过以下方式轻松地解决&#xff1a;


  • 编写执行配置&#xff0c;使其创建的线程数超过执行分配工作所需的线程数&#xff1b;
  • 将一个值作为参数传递到核函数 (N) 中&#xff0c;该值表示要处理的数据集总大小或完成工作所需的总线程数&#xff1b;
  • 计算网格内的线程索引后&#xff08;使用 threadIdx &#43; blockIdx*blockDim&#xff09;&#xff0c;请检查该索引是否超过 N&#xff0c;并且只在不超过的情况下执行与核函数相关的工作。

以下是编写执行配置的惯用方法示例&#xff0c;适用于 N 和线程块中的线程数已知&#xff0c;但无法保证网格中的线程数和 N 之间完全匹配的情况。如此一来&#xff0c;便可确保网格中至少始终拥有 N 所需的线程数&#xff0c;且超出的线程数至多仅可相当于 1 个线程块的线程数量&#xff1a;

// Assume &#96;N&#96; is known
int N &#61; 100000;
// Assume we have a desire to set &#96;threads_per_block&#96; exactly to &#96;256&#96;
size_t threads_per_block &#61; 256;
// Ensure there are at least &#96;N&#96; threads in the grid, but only 1 block&#39;s worth extra
size_t number_of_blocks &#61; (N &#43; threads_per_block - 1) / threads_per_block;
some_kernel<<<number_of_blocks, threads_per_block>>>(N);

由于上述执行配置致使网格中的线程数超过 N&#xff0c;因此需要注意 some_kernel 定义中的内容&#xff0c;以确保 some_kernel 在由其中一个 ”额外的” 线程执行时不会尝试访问超出范围的数据元素&#xff1a;

__global__ some_kernel(int N)

int idx &#61; threadIdx.x &#43; blockIdx.x * blockDim.x;
if (idx < N) // Check to make sure &#96;idx&#96; maps to some value within &#96;N&#96;

// Only do work if it does





跨网格的循环

数据元素数量往往会大于网格中的线程数&#xff0c;在此类情况下&#xff0c;线程无法只处理一个元素&#xff0c;否则工作便无法完成。


以编程方式解决此问题的其中一种方法是使用网格跨度循环&#xff0c;在网格跨度循环中&#xff0c;线程的第一个元素依旧使用 threadIdx.x &#43; blockIdx.x * blockDim.x 计算得出。然后&#xff0c;线程会按网格中的线程数 (blockDim.x * gridDim.x) 向前迈进&#xff0c;在本例中线程数为 8。


线程会继续向前迈进&#xff0c;直至其数据索引超出数据元素的数量&#xff0c;所有线程均按此种方式运作&#xff0c;如此便会涵盖所有元素。


数据集比网格大

或出于选择&#xff0c;为了要创建具有超高性能的执行配置&#xff0c;或出于需要&#xff0c;一个网格中的线程数量可能会小于数据集的大小。请思考一下包含 1000 个元素的数组和包含 250 个线程的网格&#xff08;此处使用极小的规模以便于说明&#xff09;。此网格中的每个线程将需使用 4 次。如要实现此操作&#xff0c;一种常用方法便是在核函数中使用跨网格循环。

在跨网格循环中&#xff0c;每个线程将在网格内使用 threadIdx &#43; blockIdx*blockDim 计算自身唯一的索引&#xff0c;并对数组内该索引的元素执行相应运算&#xff0c;然后将网格中的线程数添加到索引并重复此操作&#xff0c;直至超出数组范围。例如&#xff0c;对于包含 500 个元素的数组和包含 250 个线程的网格&#xff0c;网格中索引为 20 的线程将执行如下操作&#xff1a;


  • 对包含 500 个元素的数组的元素 20 执行相应运算&#xff1b;
  • 将其索引增加 250&#xff0c;使网格的大小达到 270&#xff1b;
  • 对包含 500 个元素的数组的元素 270 执行相应运算&#xff1b;
  • 将其索引增加 250&#xff0c;使网格的大小达到 520&#xff1b;
  • 由于 520 现已超出数组范围&#xff0c;因此线程将停止工作。

CUDA 提供一个可给出网格中线程块数的特殊变量&#xff1a;gridDim.x。然后计算网格中的总线程数&#xff0c;即网格中的线程块数乘以每个线程块中的线程数&#xff1a;gridDim.x * blockDim.x 。带着这样的想法来看看以下核函数中网格跨度循环的详细示例&#xff1a;

__global void kernel(int *a, int N)

int indexWithinTheGrid &#61; threadIdx.x &#43; blockIdx.x * blockDim.x;
int gridStride &#61; gridDim.x * blockDim.x;
for (int i &#61; indexWithinTheGrid; i < N; i &#43;&#61; gridStride)

// do work on a[i];





错误处理

与在任何应用程序中一样&#xff0c;加速 CUDA 代码中的错误处理同样至关重要。即便不是大多数&#xff0c;也有许多 CUDA 函数&#xff08;例如&#xff0c;内存管理函数&#xff09;会返回类型为 cudaError_t 的值&#xff0c;该值可用于检查调用函数时是否发生错误。以下是对调用 cudaMallocManaged 函数执行错误处理的示例&#xff1a;

cudaError_t err;
err &#61; cudaMallocManaged(&a, N) // Assume the existence of &#96;a&#96; and &#96;N&#96;.
if (err !&#61; cudaSuccess) // &#96;cudaSuccess&#96; is provided by CUDA.

printf("Error: %s\\n", cudaGetErrorString(err)); // &#96;cudaGetErrorString&#96; is provided by CUDA.

启动定义为返回 void 的核函数后&#xff0c;将不会返回类型为 cudaError_t 的值。为检查启动核函数时是否发生错误&#xff08;例如&#xff0c;如果启动配置错误&#xff09;&#xff0c;CUDA 提供 cudaGetLastError 函数&#xff0c;该函数会返回类型为 cudaError_t 的值。

/*
* This launch should cause an error, but the kernel itself
* cannot return it.
*/

someKernel<<<1, -1>>>(); // -1 is not a valid number of threads.
cudaError_t err;
err &#61; cudaGetLastError(); // &#96;cudaGetLastError&#96; will return the error from above.
if (err !&#61; cudaSuccess)

printf("Error: %s\\n", cudaGetErrorString(err));

最后&#xff0c;为捕捉异步错误&#xff08;例如&#xff0c;在异步核函数执行期间&#xff09;&#xff0c;请务必检查后续同步 CUDA 运行时 API 调用所返回的状态&#xff08;例如 cudaDeviceSynchronize&#xff09;&#xff1b;如果之前启动的其中一个核函数失败&#xff0c;则将返回错误。



自定义一个CUDA错误处理宏


#include
#include
inline cudaError_t checkCuda(cudaError_t result)

if (result !&#61; cudaSuccess)
fprintf(stderr, "CUDA Runtime Error: %s\\n", cudaGetErrorString(result));
assert(result &#61;&#61; cudaSuccess);

return result;

int main()

/*
* The macro can be wrapped around any function returning
* a value of type &#96;cudaError_t&#96;.
*/

checkCuda( cudaDeviceSynchronize() )







推荐阅读
  • 本文介绍了brain的意思、读音、翻译、用法、发音、词组、同反义词等内容,以及脑新东方在线英语词典的相关信息。还包括了brain的词汇搭配、形容词和名词的用法,以及与brain相关的短语和词组。此外,还介绍了与brain相关的医学术语和智囊团等相关内容。 ... [详细]
  • 本文介绍了PhysioNet网站提供的生理信号处理工具箱WFDB Toolbox for Matlab的安装和使用方法。通过下载并添加到Matlab路径中或直接在Matlab中输入相关内容,即可完成安装。该工具箱提供了一系列函数,可以方便地处理生理信号数据。详细的安装和使用方法可以参考本文内容。 ... [详细]
  • C++字符字符串处理及字符集编码方案
    本文介绍了C++中字符字符串处理的问题,并详细解释了字符集编码方案,包括UNICODE、Windows apps采用的UTF-16编码、ASCII、SBCS和DBCS编码方案。同时说明了ANSI C标准和Windows中的字符/字符串数据类型实现。文章还提到了在编译时需要定义UNICODE宏以支持unicode编码,否则将使用windows code page编译。最后,给出了相关的头文件和数据类型定义。 ... [详细]
  • 本文讨论了在iOS平台中的Metal框架中,对于if语句中的判断条件的限制和处理方式。作者提到了在Metal shader中,判断条件不能写得太长太复杂,否则可能导致程序停留或没有响应。作者还分享了自己的经验,建议在CPU端进行处理,以避免出现问题。 ... [详细]
  • 本文详细介绍了如何使用MySQL来显示SQL语句的执行时间,并通过MySQL Query Profiler获取CPU和内存使用量以及系统锁和表锁的时间。同时介绍了效能分析的三种方法:瓶颈分析、工作负载分析和基于比率的分析。 ... [详细]
  • 带添加按钮的GridView,item的删除事件
    先上图片效果;gridView无数据时显示添加按钮,有数据时,第一格显示添加按钮,后面显示数据:布局文件:addr_manage.xml<?xmlve ... [详细]
  • Tkinter Frame容器grid布局并使用Scrollbar滚动原理
    本文介绍了如何使用Tkinter实现Frame容器的grid布局,并通过Scrollbar实现滚动效果。通过将Canvas作为父容器,使用滚动Canvas来滚动Frame,实现了在Frame中添加多个按钮,并通过Scrollbar进行滚动。同时,还介绍了更新Frame大小和绑定滚动按钮的方法,以及配置Scrollbar的相关参数。 ... [详细]
  • Android图形架构学习笔记(待修改)
    以下简单总结来自Android官网,稍作总结:https:source.android.google.cndevicesgraphics概览Andr ... [详细]
  • 关于如何快速定义自己的数据集,可以参考我的前一篇文章PyTorch中快速加载自定义数据(入门)_晨曦473的博客-CSDN博客刚开始学习P ... [详细]
  • 在Android开发中,使用Picasso库可以实现对网络图片的等比例缩放。本文介绍了使用Picasso库进行图片缩放的方法,并提供了具体的代码实现。通过获取图片的宽高,计算目标宽度和高度,并创建新图实现等比例缩放。 ... [详细]
  • Python正则表达式学习记录及常用方法
    本文记录了学习Python正则表达式的过程,介绍了re模块的常用方法re.search,并解释了rawstring的作用。正则表达式是一种方便检查字符串匹配模式的工具,通过本文的学习可以掌握Python中使用正则表达式的基本方法。 ... [详细]
  • 展开全部下面的代码是创建一个立方体Thisexamplescreatesanddisplaysasimplebox.#Thefirstlineloadstheinit_disp ... [详细]
  • CentOS 7部署KVM虚拟化环境之一架构介绍
    本文介绍了CentOS 7部署KVM虚拟化环境的架构,详细解释了虚拟化技术的概念和原理,包括全虚拟化和半虚拟化。同时介绍了虚拟机的概念和虚拟化软件的作用。 ... [详细]
  • Android工程师面试准备及设计模式使用场景
    本文介绍了Android工程师面试准备的经验,包括面试流程和重点准备内容。同时,还介绍了建造者模式的使用场景,以及在Android开发中的具体应用。 ... [详细]
  • 嵌入式处理器的架构与内核发展历程
    本文主要介绍了嵌入式处理器的架构与内核发展历程,包括不同架构的指令集的变化,以及内核的流水线和结构。通过对ARM架构的分析,可以更好地理解嵌入式处理器的架构与内核的关系。 ... [详细]
author-avatar
cecillalurw_689
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有